package xj.graph2d.util;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.LookupOp;
import java.awt.image.ShortLookupTable;

import org.jdesktop.swingx.graphics.ShadowRenderer;

public class DrawingUtilities implements xj.graph2d.DrawShapeConstants {

  public static void drawTriangle(Graphics2D g2, 
				  float x1, float y1, 
				  float x2, float y2, 
				  float x3, float y3, 
				  boolean fill) {
    GeneralPath path = new GeneralPath();
    path.moveTo(x1, y1);
    path.lineTo(x2, y2);
    path.lineTo(x3, y3);
    path.lineTo(x1, y1);
    if (fill) {
      g2.fill(path);
    } else {
      g2.draw(path);
    }
  }

  public static void drawTriangle(Graphics2D g2, 
				  float x1, float y1, 
				  float x2, float y2, 
				  float x3, float y3) {
    drawTriangle(g2, x1, y1, x2, y2, x3, y3, false);
  }

  public static void fillTriangle(Graphics2D g2, 
				  float x1, float y1, 
				  float x2, float y2, 
				  float x3, float y3) {
    drawTriangle(g2, x1, y1, x2, y2, x3, y3, true);
  }

  public static void drawOverFrame(Graphics2D g2, Rectangle2D rect) {
    drawFrame(g2, rect, null, overColor, true);
  }

  public static void drawGroupFrame(Graphics2D g2, Rectangle2D rect,
				    boolean fill) {
    drawFrame(g2, rect, Color.lightGray, groupColor, fill);
  }

  public static void drawBoxFrame(Graphics2D g2, Rectangle2D rect, boolean fill) {
    drawFrame(g2, rect, Color.lightGray, groupColor, fill);
  }

  public static void drawFrame(Graphics2D g2, Rectangle2D rect,
			       Color frameColor, Color fillColor, boolean fill) {
    if (g2 != null && rect != null) {
      Stroke s = g2.getStroke();
      Color c = g2.getColor();
      if (fill) {
	if (fillColor != null) {
	  g2.setColor(fillColor);
	}
	g2.fill(rect);
      }
      if (frameColor != null) {
	g2.setColor(frameColor);
      }
      g2.setStroke(groupStroke);
      g2.draw(rect);
      g2.setStroke(s);
      g2.setColor(c);
    }
  }

  public static void drawRectFrame(Graphics2D g2, Rectangle2D bounds,
				   boolean drawrect, boolean rotatable, float marksize) {
    drawRectFrame(g2, bounds, drawrect, true, rotatable, 
		  marksize, defaultFrameColor);
  }

  public static void drawRectFrame(Graphics2D g2, Rectangle2D bounds,
				   boolean drawrect, boolean drawcorner, 
				   boolean rotatable, float marksize) {
    drawRectFrame(g2, bounds, drawrect, drawcorner, rotatable, 
		  marksize, defaultFrameColor);
  }

  public static void drawRectFrame(Graphics2D g2, Rectangle2D bounds,
				   boolean drawrect, float marksize, Color frameColor) {
    drawRectFrame(g2, bounds, drawrect, true, false, marksize, frameColor);
  }

  public static void drawRectFrame(Graphics2D g2, Rectangle2D bounds,
				   boolean drawrect, boolean drawcorner, boolean rotatable, 
				   float marksize, Color frameColor) {
    drawRectFrame(g2, bounds, drawrect, drawcorner, rotatable, marksize,
		  frameColor, anchorColor);
  }

  public static void drawRectFrame(Graphics2D g2, Rectangle2D bounds,
				   boolean drawrect, boolean drawcorner, boolean rotatable, 
				   float marksize, Color frameColor, Color handleColor) {
    if (bounds != null) {
      drawRectFrame(g2, (float) bounds.getX(), (float) bounds.getY(),
		    (float) bounds.getWidth(), (float) bounds.getHeight(), 
		    drawrect, drawcorner, rotatable, 
		    marksize, frameColor, handleColor);
    }
  }

  public static void drawRectFrame(Graphics2D g2, 
				   Point2D p1, Point2D p2,
				   boolean drawrect, boolean drawcorner, boolean rotatable, 
				   float marksize, Color frameColor, Color handleColor) {
    if (p1 != null && p2 != null) {
      float x = (float) Math.min(p1.getX(), p2.getX());
      float y = (float) Math.min(p1.getY(), p2.getY());
      float w = (float) Math.abs(p1.getX() - p2.getX());
      float h = (float) Math.abs(p1.getY() - p2.getY());
      drawRectFrame(g2, x, y, w, h, drawrect, drawcorner, rotatable, true,
		    marksize, frameColor, handleColor);
    }
  }

  public static void drawRectFrame(Graphics2D g2, 
				   Point2D p1, Point2D p2,
				   boolean drawrect, boolean drawcorner, 
				   boolean rotatable, boolean fill, 
				   float marksize, Color frameColor, Color handleColor) {
    if (p1 != null && p2 != null) {
      float x = (float) Math.min(p1.getX(), p2.getX());
      float y = (float) Math.min(p1.getY(), p2.getY());
      float w = (float) Math.abs(p1.getX() - p2.getX());
      float h = (float) Math.abs(p1.getY() - p2.getY());
      drawRectFrame(g2, x, y, w, h, drawrect, drawcorner, rotatable, fill,
		    marksize, frameColor, handleColor);
    }
  }

  public static void drawRectFrame(Graphics2D g2, 
				   float x, float y, float w, float h, 
				   boolean drawrect, boolean drawcorner, boolean rotatable,
				   float marksize, Color frameColor, Color handleColor) {
    drawRectFrame(g2, x, y, w, h, 
		  drawrect, drawcorner, rotatable, true,
		  marksize, frameColor, handleColor);
  }

  public static void drawRectFrame(Graphics2D g2, 
				   float x, float y, float w, float h, 
				   boolean drawrect, boolean drawcorner, 
				   boolean rotatable, boolean fill, 
				   float marksize, Color frameColor, Color handleColor) {
    float md = marksize / 2.0f;
    float mx[] = new float[3];
    float my[] = new float[3];

    mx[0] = x;
    mx[1] = (x + w / 2.0f);
    mx[2] = (x + w);
    my[0] = y;
    my[1] = (y + h / 2.0f);
    my[2] = (y + h);

    Color saveColor = g2.getColor();
    g2.setColor(frameColor);

    Rectangle2D r;
    if (drawrect) {
      // drawDashedLine(x1, y1, x1, y2);
      // drawDashedLine(x1, y1, x2, y1);
      // drawDashedLine(x1, y2, x2, y2);
      // drawDashedLine(x2, y1, x2, y2);
    }
    for (int i = 0; i <= 2; i++) {
      for (int j = 0; j <= 2; j++) {
	if (i != 1 || j != 1) {
	  if (drawcorner || i == 1 || j == 1) {
	    r = new Rectangle2D.Float(mx[i] - md, my[j] - md, marksize, marksize);
	    if (fill) { 
	      g2.fill(r);
	    } else { 
	      g2.draw(r);
	    }
	  }
	}
      }
    }

    if (handleColor != null && rotatable) {
      g2.setColor(handleColor);
      float hx = mx[1];
      float hy = my[0] - handleLength;

      r = new Rectangle2D.Float(hx - md, hy - md, marksize, marksize);
      if (fill) { 
	g2.fill(r);
      } else { 
	g2.draw(r);
      }
      g2.setColor(Color.gray);
      Stroke s = g2.getStroke();
      g2.setStroke(ctrStroke);
      g2.draw(new Line2D.Float(hx, hy + md, hx, my[0] - md));
      g2.setStroke(s);
    }

    g2.setColor(saveColor);

  }

  protected static final boolean fixedMarkSize = true; 

  public static int getMarkSize(int penSize) { 
    if (fixedMarkSize || penSize < 1) { 
      penSize = 1;
    }
    return penSize + (penSize % 2 == 0 ? 0 : 1) + 2 * (int) d;
  }

  public static void drawLineFrame(Graphics2D g2, Point2D p1, Point2D p2,
				   int marksize) {
    drawLineFrame(g2, p1, p2, marksize, false, false, false, false);
  }

  public static void drawLineFrame(Graphics2D g2, Point2D p1, Point2D p2,
				   int marksize, boolean markFirst, boolean markLast,
				   boolean markEdgeSource, boolean markEdgeDestination) {
    if (p1 != null && p2 != null) {
      float x1 = (float) p1.getX();
      float y1 = (float) p1.getY();
      float x2 = (float) p2.getX();
      float y2 = (float) p2.getY();
      drawLineFrame(g2, x1, y1, x2, y2, marksize, markFirst, markLast,
		    markEdgeSource, markEdgeDestination);
    }
  }

  public static void drawLineFrame(Graphics2D g2, 
				   float x1, float y1, 
				   float x2, float y2, 
				   int marksize, boolean markEdgeSource,
				   boolean markEdgeDestination) {
    drawLineFrame(g2, x1, y1, x2, y2, marksize, false, false, 
		  markEdgeSource, markEdgeDestination);
  }

  public static void drawLineFrame(Graphics2D g2, 
				   float x1, float y1, float x2, float y2, 
				   int marksize, boolean markFirst, boolean markLast,
				   boolean markEdgeSource, boolean markEdgeDestination) {
    float md = marksize / 2.0f;
    float xm = (x1 + x2) / 2.0f;
    float ym = (y1 + y2) / 2.0f;
    Color saveColor = g2.getColor();
    //Color endPointColor = anchorColor;
    //Rectangle2D r; 
    Ellipse2D e; 
    if (markFirst) {
      /*
      if (markEdgeSource) {
	endPointColor = edgeSourceColor;
      } else {
	endPointColor = edgeSourceColor; //anchorColor;
      }
      */
      g2.setColor(edgeSourceColor);
    } else {
      g2.setColor(defaultFrameColor);
    }
    //r = new Rectangle2D.Float(x1 - md, y1 - md, marksize, marksize);
    e = new Ellipse2D.Float(x1 - md, y1 - md, marksize, marksize);
    if (frameMarkFilled) { 
      g2.fill(e);
    } else { 
      g2.draw(e);
    }

    g2.setColor(defaultFrameColor);
    //r = new Rectangle2D.Float(xm - md, ym - md, marksize, marksize);
    e = new Ellipse2D.Float(xm - md, ym - md, marksize, marksize);
    if (frameMarkFilled) { 
      g2.fill(e); 
    } else { 
      g2.draw(e); 
    }

    if (markLast) {
      /*
      if (markEdgeDestination) {
	endPointColor = edgeDestinationColor;
      } else {
	endPointColor = edgeDestinationColor; //anchorColor;
      }
      */
      g2.setColor(edgeDestinationColor);
    } else {
      g2.setColor(defaultFrameColor);
    }

    //r = new Rectangle2D.Float(x2 - md, y2 - md, marksize, marksize);
    e = new Ellipse2D.Float(x2 - md, y2 - md, marksize, marksize);
    if (frameMarkFilled) { 
      g2.fill(e);
    } else { 
      g2.draw(e);
    }
    g2.setColor(saveColor);
  }

  public static final int blurPad = 20;

  public static void paintBlurFrame(Graphics2D g2, Shape shape, 
				    boolean fill, Color bcolor) {
    if (g2 != null && shape != null && bcolor != null) {
      paintBlurFrame(g2, shape, fill,
		     bcolor.getRed(), bcolor.getGreen(), 
		     bcolor.getBlue(), bcolor.getAlpha());
    }
  }

  public static void paintBlurFrame(Graphics2D g2, Shape shape, 
				    boolean fill,
				    int r, int g, int b, int a) {
    if (g2 != null && shape != null) {
      LookupOp op = makeLookupOp(r, g, b, a);
      paintBlurFrame(g2, shape, fill, op);
    }
  }

  public static void paintBlurFrame(Graphics2D g2, Shape shape) {
    if (g2 != null && shape != null) {
      paintBlurFrame(g2, shape, true, (LookupOp) null);
    }    
  }

  public static void paintBlurFrame(Graphics2D g2, Shape shape, boolean fill) {
    if (g2 != null && shape != null) {
      paintBlurFrame(g2, shape, fill, (LookupOp) null);
    }
  }

  public static void paintBlurFrame(Graphics2D g2, Shape shape, boolean fill, LookupOp op) {
    if (g2 != null && shape != null) {
      if (op == null) {
	op = makeDefaultLookupOp();
      }
      GaussianBlur blur = new GaussianBlur(4, 4);

      Rectangle2D rect = shape.getBounds2D();

      // System.out.println("paintBlurFrame(): rect=" + rect);

      BufferedImage bi = new BufferedImage((int) rect.getWidth() + 2 * blurPad,
					   (int) rect.getHeight() + 2 * blurPad, 
					   BufferedImage.TYPE_INT_ARGB);

      Graphics2D big = bi.createGraphics();
      big.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
			   RenderingHints.VALUE_ANTIALIAS_ON);
      big.setRenderingHint(RenderingHints.KEY_RENDERING,
			   RenderingHints.VALUE_RENDER_QUALITY);

      AffineTransform af = new AffineTransform();
      af.setToTranslation(-rect.getX() + blurPad, -rect.getY() + blurPad);
      Shape shape1 = af.createTransformedShape(shape);
      if (fill) { 
	big.fill(shape1);
      } else { 
	big.draw(shape1);
      }

      BufferedImage bi2 = op.filter(bi, null);

      // System.out.println("paintBlurFrame(): bi2 widht=" +
      // bi2.getWidth() + " height=" + bi2.getHeight());

      BufferedImage bi3 = blur.filter(bi2);

      // System.out.println("paintBlurFrame(): bi3 widht=" +
      // bi3.getWidth() + " height=" + bi3.getHeight());

      Graphics2D big3 = bi3.createGraphics();
      big3.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
			    RenderingHints.VALUE_ANTIALIAS_ON);
      big3.setRenderingHint(RenderingHints.KEY_RENDERING,
			    RenderingHints.VALUE_RENDER_QUALITY);
      AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.CLEAR);
      big3.setComposite(ac);
      big3.setColor(new Color(0, 0, 0, 0));
      big3.fill(shape1);
      g2.drawImage(bi3, (int) rect.getX() - blurPad, (int) rect.getY() - blurPad, null);
    }
  }

  public static LookupOp makeLookupOp(int r, int g, int b, int a) {
    short[] rband = new short[256];
    short[] gband = new short[256];
    short[] bband = new short[256];
    short[] aband = new short[256];
    for (int i = 0; i < 256; i++) {
      rband[i] = (short) r;
      gband[i] = (short) g;
      bband[i] = (short) b;
      aband[i] = (short) a;
    }
    short[][] bands = new short[][] { rband, gband, bband, aband };
    return new LookupOp(new ShortLookupTable(0, bands), null);
  }

  public static LookupOp makeDefaultLookupOp() {
    // short[] zero = new short[256];
    short[] half = new short[256];
    short[] full = new short[256];
    // short[] straight = new short[256];
    short[] straight2 = new short[256];
    for (int i = 0; i < 256; i++) {
      // zero[i] = (short) 0;
      half[i] = (short) 128;
      full[i] = (short) 255;
      // straight[i] = (short) i;
      straight2[i] = (short) (i * 3 / 4);
    }
    short[][] alpha = new short[][] { half, half, full, straight2 };
    return new LookupOp(new ShortLookupTable(0, alpha), null);
  }

  // using custom shadow drawing  
  public static void drawDropShadow2(Graphics2D g2, Shape shape) { 
    drawDropShadow2(g2, shape, //null, 
		   5, false);
  }

  public static void drawDropShadow2(Graphics2D g2, Shape shape, 
				    //Stroke stroke, 
				    int shadowPad) { 
    drawDropShadow2(g2, shape, //stroke, 
		   shadowPad, false);
  }

  public static void drawDropShadow2(Graphics2D g2, Shape shape, 
				     //Stroke stroke, 
				     int shadowPad, boolean flatShadow) { 
    if (g2 != null && shape != null) {
      Rectangle2D rect = shape.getBounds2D();
      int width = (int) rect.getWidth() + shadowPad;
      int height = (int) rect.getHeight() + shadowPad;
      BufferedImage bi = new BufferedImage(width, height, 
					   Transparency.TRANSLUCENT);
      //BufferedImage.TYPE_INT_ARGB);
      Graphics2D big = bi.createGraphics();
      big.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      //big.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
      big.setComposite(AlphaComposite.Clear);
      big.fillRect(0, 0, width, height);
            
      AffineTransform af = new AffineTransform();
      af.setToTranslation(-rect.getX() + shadowPad, -rect.getY() + shadowPad);
      Shape shape1 = af.createTransformedShape(shape);

      big.setComposite(AlphaComposite.Src);
      big.setColor(Color.WHITE);
      big.fill(shape1);
      big.setComposite(AlphaComposite.SrcAtop);

      if (flatShadow) { 
	big.setColor(Color.lightGray);
	big.fill(shape1);
      } else {       
	//big.setClip(shape1);
	af.setToTranslation(-rect.getX(), -rect.getY());
	Shape shape2 = af.createTransformedShape(shape);
	int sw = shadowPad * 2;
	for (int i = sw; i >= 2; i-=2) {
	  //float pct = (float) (sw - i) / (sw - 1);
	  float pct = (float) (sw - i + 2) / sw;
	  big.setColor(getMixedColor(Color.lightGray, pct,
				     Color.white, 1.0f-pct));
	  big.setStroke(new BasicStroke(i));
	  big.draw(shape2);
	}
      }

      af.setToTranslation(-shadowPad, -shadowPad);
      shape1 = af.createTransformedShape(shape1);      
      AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.CLEAR);
      big.setComposite(ac);
      big.setColor(new Color(0, 0, 0, 0));
      big.setStroke(g2.getStroke());
      big.fill(shape1);
      //big.draw(shape1);

      g2.drawImage(bi, (int) rect.getX(), (int) rect.getY(), null);
    }
  }

  private static Color getMixedColor(Color c1, float pct1, Color c2, float pct2) {
    float[] clr1 = c1.getComponents(null);
    float[] clr2 = c2.getComponents(null);
    for (int i = 0; i < clr1.length; i++) {
      clr1[i] = (clr1[i] * pct1) + (clr2[i] * pct2);
    }
    return new Color(clr1[0], clr1[1], clr1[2], clr1[3]);
  }

  public static Color darkerColor(Color c, float factor) {
    if (c != null) { 
      return new Color(Math.max((int)(c.getRed()   * factor), 0), 
		       Math.max((int)(c.getGreen() * factor), 0),
		       Math.max((int)(c.getBlue()  * factor), 0),
		       Math.min((c.getAlpha()  * 3), 255));
		       //c.getAlpha());
    }
    return null;
  }

  public static Color lighterColor(Color c, int d) {
    if (c != null) { 
      return new Color(Math.min((c.getRed()   + d), 255), 
		       Math.min((c.getGreen() + d), 255),
		       Math.min((c.getBlue()  + d), 255),
		       Math.min((c.getAlpha()  * 3), 255));
		       //c.getAlpha());
    }
    return null;
  }

  // using SwingX ShadowRender
  public static void drawDropShadow(Graphics2D g2, 
				    Shape shape, 
				    boolean fill,
				    int shadowSize) { 
    if (g2 != null && shape != null) {
      Rectangle2D rect = shape.getBounds2D();
      int width = (int) rect.getWidth() + 2 * shadowSize;
      int height = (int) rect.getHeight() + 2 * shadowSize;
      int x = (int) rect.getX();
      int y = (int) rect.getY();

      AffineTransform af = new AffineTransform();
      //af.setToTranslation(-x + shadowSize, -y + shadowSize);
      af.setToTranslation(-x, -y);
      Shape shape1 = af.createTransformedShape(shape);

      BufferedImage bi = new BufferedImage(width, height, Transparency.TRANSLUCENT);     
      Graphics2D big = bi.createGraphics();
      big.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      big.setColor(Color.black);
      if (fill) { 
	big.fill(shape1);
      } else { 
	big.draw(shape1);
      }

      ShadowRenderer renderer = new ShadowRenderer(shadowSize, 0.5f, Color.darkGray);
      BufferedImage shadow = renderer.createShadow(bi);
      big = shadow.createGraphics();
      big.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.CLEAR);
      big.setComposite(ac);
      big.setColor(new Color(0, 0, 0, 0));
      big.fill(shape1);

      g2.drawImage(shadow, x, y, null);
    }
  }

  /*
   * (x, y) is the anchor point. Calculate the angle between x-axis and
   * the line from (x, y) to (vx, vy). Result in rangle [ 0, 2 * PI )
   */
  public static double calculateRotationAngle(float x, float y, float vx, float vy) {
    double t = 0;
    if (vy == y) {
      if (vx >= x) {
	t = 0;
      } else {
	t = Math.PI;
      }
    } else if (vx == x) {
      if (vy >= y) {
	t = Math.PI / 2;
      } else {
	t = Math.PI * 1.5;
      }
    } else {
      double x0 = Math.abs(vx - x);
      double y0 = Math.abs(vy - y);
      double t0 = Math.atan(y0 / x0);
      if (vy > y) {
	if (vx > x) {
	  t = t0;
	} else {
	  t = Math.PI - t0;
	}
      } else {
	if (vx > x) {
	  t = 2 * Math.PI - t0;
	} else {
	  t = Math.PI + t0;
	}
      }
    }
    return t;
  }

}
